Next.js 目前已經支援 TypeScript,而且從 GitHub 中可以看到 TypeScript 占整體 codebase 的比例逐漸變高,所以不用擔心在 Next.js 不能使用 TypeScript 的問題。
在這篇文章中將紀錄在 Next.js 中常見的 TypeScript 寫法,看文這篇文章後你將會學到:
next.config.js
的型別Next.js 跟許多從 JavaScript 轉移到 TypeScript 的套件不太一樣,沒有另外安裝 @types/next
的套件,而是直接在根目錄使用 next-env.d.ts
這個檔案引用型別
/// <reference types="next" />
/// <reference types="next/types/global" />
這個檔案中使用 typescript 的 triple-slash directives,引用了 Next.js 所定義的型別。再看到 tsconfig.json
中的 include
包含了這個檔案,所以你現在知道,這個檔案會參與 TypeScript 的編譯過程。
第一行引用的是 next/types/index.d.ts
,在這個檔案中在額外引用了一些型別,基本上都是在撰寫 React 使用得到的:
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
/// <reference types="styled-jsx" />
此外,還包含了許多在建構 Next.js 應用時會到的型別定義,例如 GetServerSideProps
、 GetStaticProps
等等。以及讓我們可以使用以下三種語法:
<html amp="">
<link nonce="">
<style jsx>
第二行引用的是 node_modules/next/types/global.d.ts
,在這個檔案中宣告了:
declare module '*.module.css' { ... }
declare module '*.module.sass' { ... }
declare module '*.module.scss' { ... }
所以我們不用額外設定,TypeScript 就可以支援 CSS 檔案跟 CSS Modules,像是 *.module.css
這種形式的檔案。如果把 next-env-d.ts
的第二行刪掉後,你會看到 typescript 無法解析像是 Home.module.css
這種檔案。
在官方文件中有簡略提到如何針對 getStaticProps
、 getStaticPaths
與 getServerSideProps
三者設定型別:
import { GetStaticProps, GetStaticPaths, GetServerSideProps } from "next";
export const getStaticProps: GetStaticProps = async (context) => {
// ...
};
export const getStaticPaths: GetStaticPaths = async () => {
// ...
};
export const getServerSideProps: GetServerSideProps = async (context) => {
// ...
};
但是實際上這樣的型別定義不是很嚴謹,以 getServerSideProps
違例,我們進一步看到它的原始型別定義。:
type ParsedUrlQuery = {
[key: string]: T | undefined;
}
type GetServerSidePropsResult<P> =
| { props: P }
| { redirect: Redirect }
| { notFound: true }
export type GetServerSideProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery
> = (
context: GetServerSidePropsContext<Q>
) => Promise<GetServerSidePropsResult<P>>
getServerSideProps
回傳的型別是 GetServerSidePropsResult
,這個回傳值可以是 props
、 redirect
或 notFound
三者其一,看到 props
的型別實際上預設的是一個很簡易的物件定義:
{ [key: string]: any }
當你看到 any
時就會知道實際上 props
不論回傳什麼都是合法的,getServerSideProps
的實作就會有些脆弱,很容易就會發生改錯卻沒發現的情況,儘管在 Next.js 預設的 TypeScript 就有開啟 strict mode,但是由於這是在 Next.js 內部的型別定義,所以 strict mode 並無法影響。
我們使用 VS Code 開啟 Next.js 專案,打開一個 SSR 的頁面,將滑鼠移動到 props
上方,此時如同上面看到的型別定義,現在不論 props
裡面的物件帶得對不對,都可以通過 TypeScript 的型別檢驗,如此一來程式碼就顯得不嚴謹。
比較好的型別定義是在使用 GetServerSideProps
時也同時傳入兩個範型,第一個範型 P
決定的是 props
的回傳型別,如下方的例子中可以看到回傳型別定義為 { post: PostData }
,假設沒有在 props
中回傳符合 PostData
的物件就會無法通過 TypeScript 的型別檢驗。
第二個範型則是可以指定目前網址上 query string 的型別,從上方的範例中可以看到 query string 原始的型別定義與 props
差不多,可以用來匹配任何的 query string。但是如此一來我們就無法精準的使用 params
物件,可能會在取值的時候發生錯誤。
從下方的範例中可以看到另外定義的 Params
型別包含了 { id: string }
,此時在 getServerSideProsp
中就可以使用 [params.id](http://params.id)
取值,傳入到 getPost
的參數也可以被正確地指定型別。
import { ParsedUrlQuery } from "querystring";
type Props = {
post: PostData;
};
interface Params extends ParsedUrlQuery {
id: string;
}
export const getServerSideProps: GetServerSideProps<Props, Params> = async (
context
) => {
// ! is a non-null/non-undefined assertion
const params = context.params!;
const post = await getPost(params.id);
return {
props: { post },
};
};
有一個地方可以注意的是在取得 params
時使用的 non-null/non-undefined assertion,這個是 TypeScript 的一個 feature,可以被用來指定一個屬性絕對不會是 null | undefined
。在 Next.js 中使用的是 file-based routing,在 [id].tsx
的頁面中,我們知道 context.params
絕對帶有 id
這個屬性,但是因為原始的型別定義讓 params
可能是 undefined
,因此便無法順利從 context
拿到 params
這個屬性,會發生 Object is possibly 'undefined'.ts(2532)
這個錯誤。
在一般的 custom App 使用 TypeScript 非常簡單,只需要從 next/app
中取出 AppProps
即可:
import { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default MyApp;
這是一個在 Next.js 中很好用的 pattern,可以被用來抽象 layout 的程式碼,在頁面中設定 getLayout
後,在 pages/_app.tsx
中使用該 getLayout
渲染 layout,這樣才可以避免在切換頁面時造成用共 layout 的狀態消失,在前面幾天我們有討論過這個議題。
由於我們需要從 Component
中取得 getLayout
這個 function,但是原始的型別定義中並沒有包含這個 getLayout
,這是我們自己額外定義的。同時在頁面上也許要加入 getLayout
這個 function,因此才能在 _app.tsx
中拿到它。
而同樣地在頁面中或是 _app.tsx
都沒有 getLayout
這個型別,所以我們必須要幫兩者自定義新的型別,分別為 AppPropsWithLayout
與 NextPageWithLayout
:
// _app.tsx
import { AppPropsWithLayout } from "next/app";
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page);
return getLayout(<Component {...pageProps}></Component>);
}
export default MyApp;
// pages/index.tsx
import { NextPageWithLayout } from "next";
import { getLayout } from "@/components/Layout";
const Home: NextPageWithLayout = () => {
return <div>You are in /</div>;
};
Home.getLayout = getLayout;
export default Home;
我們可以在跟目錄創建一個檔案 next.d.ts
,這個檔案將會被用來在 next
中新增兩個新的型別,分別為 NextPageWithLayout
與 AppPropsWithLayout
:
// next.d.ts
import { NextPageWithLayout } from "next";
import { AppProps } from "next/app";
declare module "next" {
type NextPageWithLayout<P> = NextPage<P> & {
getLayout?: (page: ReactElement) => ReactNode;
};
}
declare module "next/app" {
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
}
在 NextPageWithLayout
主要就是讓 NextPage
這個型別加上 getLayout
這個 function,可以傳入一個類似 HOC 的 function。而在 AppPropsWithLayout
可以直接拿 NextPageWithLayout
來用,在這個檔案的最上面能夠看到我們在 module 'next'
中定義的 NextPageWithLayout
被 import 進來使用。
目前在使用 custom Document 還是建議使用 class component 的形式,而在 class component 中需要定義型別的部分很少,從官方文件中只有提到 getInitialProps
中的 ctx
需要定義其型別為 DocumentContext
:
import Document, { DocumentContext } from "next/document";
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return initialProps;
}
}
export default MyDocument;
各位讀者可能會問, custom Document 可以是 functional component 的形式嗎?答案是可以的,從 PR#28515 可以發現 function component 的使用案例已經被 merge 到 11.1.1 版本中,但是目前支援度不是很好,許多 React hooks 都不能使用,而且型別定義也還有改善的空間,等之後官方文件中有特別寫道 functional component 的使用個案時再採取這個方案可能會比較好。
在 API routes 使用 TypeScript 的方式也是開箱即可使用,從 next
中 import NextApiRequest
與 NextApiResponse
分別用來定義 API routes 的兩個參數:
import { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default (req: NextApiRequest, res: NextApiResponse<Data>) => {
res.status(200).json({ name: "John Doe" });
};
在定義 res
的型別時可以傳入一個範型,它會被用來定義回傳值的物件型別,從上方的範例中就可以看到回傳物件會是 { name: string }
,透過定義回傳值的型別可以讓 API routes 的程式碼更為嚴謹,比較不怕會改壞它。
根據官方說明, next.config.js
必須是 .js
檔案,目前無法原生支援 TypeScript,如果要讓這個檔案也有型別檢查可以透過以下方式,讓 IDE 幫我們指出哪個屬性寫錯了:
// @ts-check
/**
* @type {import('next').NextConfig}
*/
const config = {
// your config here
};
module.exports = config;
舉一個例子,假設我們傳入 env: true
,但是與原始型別定義不符,就可以讓 IDE 幫我們指出 env
型別有誤: